package com.zws.cucumber.glue;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.ClasspathFileSource;
import com.github.tomakehurst.wiremock.common.Json;
import com.github.tomakehurst.wiremock.http.*;
import com.github.tomakehurst.wiremock.matching.RequestPattern;
import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
import com.github.tomakehurst.wiremock.standalone.JsonFileMappingsSource;
import com.github.tomakehurst.wiremock.stubbing.*;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import com.github.tomakehurst.wiremock.verification.NearMiss;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import net.javacrumbs.jsonunit.JsonAssert;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import com.zws.cucumber.ZwsConstants;
import com.zws.cucumber.expr.ZwsExpr;
import com.zws.cucumber.util.ZwsMockUtil;
import com.zws.cucumber.util.colorfullog.ZwsColorfulLogger;
import com.zws.cucumber.util.colorfullog.ZwsPrintTableHelper;
import com.zws.cucumber.var.ZwsVar;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.AbstractMap.SimpleEntry;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import static com.google.common.base.MoreObjects.firstNonNull;
import static java.util.stream.Collectors.*;
import static net.javacrumbs.jsonunit.JsonAssert.when;
import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER;
import static org.junit.jupiter.api.Assertions.*;
import static com.zws.cucumber.expander.ZwsStr.expand;
import static com.zws.cucumber.expr.ZwsExpr.evaluate;

/**
 * author: zws
 */
public class ZwsWiremockStepDef {

    private static final Logger logger = ZwsColorfulLogger.of(ZwsWiremockStepDef.class);

    public static final String VAR_WIREMOCK_FOUND_REQUESTS = "__wiremock_found_requests__";

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired(required = false)
    private WireMockServer wireMockServer;

    @Given("wiremock stub for {string}")
    public void stub(String filepath) throws URISyntaxException, IOException {
        logger.debug("stub, filepath: {}", filepath = expand(filepath));

        StubMapping stubMapping = ZwsMockUtil.loadMapping(filepath, null);
        register(stubMapping);
        logger.debug("done");
    }

    @Given("wiremock stub for {string} with params")
    public void stub(String filepath, DataTable dataTable) throws URISyntaxException, IOException {
        logger.debug("stub with params, filepath: {}", filepath = expand(filepath));
        Map<String, Object> params = new HashMap<>();
        if (dataTable != null ) {
            dataTable.asMap(String.class, String.class).forEach((key, value) -> {
                params.put(key, evaluate(key, expand(value)));
            });

        }
        StubMapping stubMapping = ZwsMockUtil.loadMapping(filepath, params);
        register(stubMapping);
        logger.debug("done");
    }

    @Given("mock {word} with {string}")
    public void mock(String serviceName, String filePath) throws IOException {
        logger.debug("mock {}, filepath: {}", serviceName, filePath = expand(filePath));
        StubMapping mapping = ZwsMockUtil.loadMappingWithExpansionAndEvaluation(filePath, null);
        register(mapping);
    }

    @Given("mock {word} with {string} matching {string}")
    public void mock(String serviceName, String filePath, String requestPatternJson) throws IOException {
        logger.debug("mock {}, filepath: {}, matching: {}", serviceName, filePath = expand(filePath), requestPatternJson);
        StubMapping mapping = ZwsMockUtil.loadMappingWithExpansionAndEvaluation(filePath, null);
        RequestPattern pattern = Json.read(expand(requestPatternJson), RequestPattern.class);
        mapping.setRequest(pattern);
        register(mapping);
    }

    @Given("mock {word} with template {string}")
    public void mockWithTemplate(String serviceName, String filePath, DataTable dataTable) throws IOException {
        logger.debug("mock {} with template, filepath: {}", serviceName, filePath = expand(filePath));
        StubMapping mapping = ZwsMockUtil.loadMappingWithExpansionAndEvaluation(filePath, dataTable.asMap(String.class, String.class));
        register(mapping);
    }

    @Given("mock {word} with template {string} matching {string}")
    public void mockWithTemplate(String serviceName, String filePath, String requestPatternJson, DataTable dataTable) throws IOException {
        logger.debug("mock {} with template, filepath: {}, matching: {}", serviceName, filePath = expand(filePath), requestPatternJson);
        StubMapping mapping = ZwsMockUtil.loadMappingWithExpansionAndEvaluation(filePath, dataTable.asMap(String.class, String.class));
        RequestPattern pattern = Json.read(expand(requestPatternJson), RequestPattern.class);
        mapping.setRequest(pattern);
        register(mapping);
    }

    @Given("mock {word} with template {string} using switches {string}")
    public void mockWithTemplate(String serviceName, String filePath, String switchStr) throws IOException {
        Map<String, String> switchMap = StringUtils.isBlank(switchStr)
            ? new HashMap<>()
            : Arrays.stream(switchStr.split(","))
                    .map(String::trim)
                    .collect(Collectors.toMap(s -> s, s -> "true"));

        StubMapping mapping = ZwsMockUtil.loadMappingWithExpansionAndEvaluation(filePath, switchMap);
        register(mapping);
    }

    @Given("register mocked service {word} with service available")
    public void registerMockedServiceAvailable(String serviceName) {
        // fixme: to be implemented @ZWS
    }

    @Given("register mocked service {word} with service unavailable")
    public void registerMockedServiceUnavailable(String serviceName) {
        // fixme: to be implemented @ZWS
    }

    @Given("register mocked service {word} with service unreachable")
    public void registerMockedServiceUnreachable(String serviceName) {
        // fixme: to be implemented @ZWS
    }

    @Given("verify invoking mock {int} times")
    public void verify(int expected, String requestPatternJson) {
        RequestPattern pattern = Json.read(expand(requestPatternJson), RequestPattern.class);
        wireMockServer.verify(expected, RequestPatternBuilder.like(pattern));
    }

    @Given("verify invoking mock {int} times {string}")
    public void verifyWithSingleLine(int expected, String requestPattern) {
        verify(expected, requestPattern);
    }

    @Given("verify invoking mock {string} times")
    public void verify(String expected, String requestPatternJson) {
        verify(Integer.parseInt(expand(expected)), requestPatternJson);
    }

    @Given("verify invoking mock {string} times {string}")
    public void verifyWithSingleLine(String expected, String requestPattern) {
        verify(expected, requestPattern);
    }

    @Then("find wiremock requests matching {string}")
    public void findWiremockRequests(String pattern) throws Exception {

        List<LoggedRequest> foundRequests = new ArrayList<>(wireMockServer.findRequestsMatching(Json.read(expand(pattern), RequestPattern.class)).getRequests());
        foundRequests.sort(Comparator.comparingLong(r -> r.getLoggedDate().getTime()));
        ZwsVar.put(VAR_WIREMOCK_FOUND_REQUESTS, foundRequests);
    }

    @Given("define {word} from wiremock request body matching {string}")
    public void defineFromWiremockRequestBodyMatching(String variableName, String requestPatternJson) {
        ZwsVar.put(variableName, getWiremockRequestBody(requestPatternJson));
    }

    @Given("define {word} from wiremock request body property {string} matching {string}")
    public void defineFromWiremockRequestBodyPropMatching(String variableName, String propertyPath, String requestPatternJson) throws JsonProcessingException {
        String body = getWiremockRequestBody(requestPatternJson);
        if (StringUtils.isNotEmpty(body)) {
            Object obj = JsonPath.read(body, propertyPath);
            ZwsVar.put(variableName, objectMapper.writeValueAsString(obj));
        } else {
            logger.warn("body is null or empty, skip");
        }
    }

    @Given("define {word} from wiremock response body matching {string}")
    public void defineFromWiremockResponseBodyMatching(String variableName, String requestPatternJson) {
        ZwsVar.put(variableName, getWiremockResponseBody(requestPatternJson));
    }

    @Given("define {word} from wiremock response body property {string} matching {string}")
    public void defineFromWiremockResponseBodyPropMatching(String variableName, String propertyPath, String requestPatternJson) throws JsonProcessingException {
        String body = getWiremockResponseBody(requestPatternJson);
        if (StringUtils.isNotEmpty(body)) {
            Object obj = JsonPath.read(body, propertyPath);
            ZwsVar.put(variableName, objectMapper.writeValueAsString(obj));
        } else {
            logger.warn("body is null or empty, skip");
        }
    }

    private LoggedRequest getWiremockLoggedRequest(String requestPatternJson) {
        RequestPattern pattern = Json.read(expand(requestPatternJson), RequestPattern.class);
        List<LoggedRequest> requestList = wireMockServer.findRequestsMatching(pattern).getRequests();
        if (requestList.isEmpty()) {
            return null;
        }
        if (requestList.size() > 1) {
            List<String> keyList = Arrays.asList("Found Request");
            List<Map<String, Object>> rowList = requestList
                    .stream()
                    .map(r -> {
                        Map<String, Object> map = new HashMap<>();
                        map.put("Found Request", Json.write(new LoggedRequestWrapper(r)).trim());
                        return map;
                    })
                    .toList();
            ZwsPrintTableHelper.printTable0("WireMock All Found Requests", Pair.of(keyList, rowList));
        }
        return requestList.get(0);
    }

    private LoggedResponse getWiremockLoggedResponse(String requestPatternJson) {
        LoggedRequest request = getWiremockLoggedRequest(requestPatternJson);
        for (ServeEvent event : wireMockServer.getAllServeEvents()) {
            if (request == event.getRequest()) {
                return event.getResponse();
            }
        }
        return null;
    }

    private String getWiremockRequestBody(String requestPatternJson) {
        LoggedRequest request = getWiremockLoggedRequest(requestPatternJson);
        return request == null ? null : request.getBodyAsString();
    }

    private String getWiremockResponseBody(String requestPatternJson) {
        LoggedResponse response = getWiremockLoggedResponse(requestPatternJson);
        return response == null ? null : response.getBodyAsString();
    }

    @After("@EndWithPrintingWireMockRequestJournal")
    @Given("print wiremock request journal")
    public void printWiremockRequestJournal() {
        List<LoggedRequest> requestList = wireMockServer.findAll(RequestPatternBuilder.allRequests());
        List<String> keyList = Arrays.asList("Request");
        List<Map<String, Object>> rowList = requestList
                .stream()
                .map(r -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("Request", Json.write(new LoggedRequestWrapper(r)).trim());
                    return map;
                })
                .toList();
        ZwsPrintTableHelper.printTable0("WireMock All Requests", Pair.of(keyList, rowList));
    }

    @After("@EndWithPrintingWireMockServeEvent")
    public void printWiremockServeEvent() {
        List<ServeEvent> requestList = wireMockServer.getAllServeEvents();
        List<String> keyList = Arrays.asList("Serve Event");
        List<Map<String, Object>> rowList = requestList
                .stream()
                .map(e -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("Serve Event",
                            new StringBuilder()
                                    .append("Request:\n")
                                    .append(Json.write(new LoggedRequestWrapper(e.getRequest())).trim())
                                    .append("\n\n")
                                    .append("Stub Mapping Id:\n")
                                    .append(e.getStubMapping().getUuid())
                                    .append("\n\n")
                                    .append("Response:\n")
                                    .append(Json.write(new LoggedResponseWrapper(e.getResponse())).trim())
                    );
                    return map;
                })
                .toList();
        ZwsPrintTableHelper.printTable0("WireMock All Serve Event", Pair.of(keyList, rowList));
    }

    @Deprecated
    @Given("print wiremock request journal matching {string}")
    public void printWiremockRequestJournal(String pattern) {
        logger.debug("wiremock request journal: {}", wireMockServer.findRequestsMatching(Json.read(expand(pattern), RequestPattern.class)).getRequests());
    }

    @Deprecated
    @After("@EndWithPrintingWireMockRequestScenarios")
    @Given("print wiremock request scenarios")
    public void printWiremockScenarios() {
        logger.debug("wiremock scenarios: {}", wireMockServer.getAllScenarios().getScenarios());
    }

    @After("@EndWithPrintingWireMockStubMappings")
    @Given("print wiremock stub mappings")
    public void printWiremockStubMappings() {
        List<StubMapping> mappingList = wireMockServer.getStubMappings();
        List<String> keyList = Arrays.asList("Mapping");
        List<Map<String, Object>> rowList = mappingList
                .stream()
                .map(m -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("Mapping", StubMapping.buildJsonStringFor(m));
                    return map;
                })
                .toList();
        ZwsPrintTableHelper.printTable0("WireMock Stub Mappings", Pair.of(keyList, rowList));
    }

    @After("@EndWithPrintingWireMockUnmatchedRequestsAndNearMisses")
    @Given("print wiremock unmatched requests and near misses")
    public void printWiremockUnmatchedRequestsAndNearMisses() {
        /*
         * All Unmatched Requests
         */
        List<LoggedRequest> unmatchedRequests = wireMockServer.findAllUnmatchedRequests();
        List<String> keyList = Arrays.asList("Unmatched Request");
        List<Map<String, Object>> rowList = unmatchedRequests
                .stream()
                .map(r -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("Unmatched Request", Json.write(new LoggedRequestWrapper(r)).trim());
                    return map;
                })
                .toList();
        ZwsPrintTableHelper.printTable0("WireMock Unmatched Requests", Pair.of(keyList, rowList));


        /*
         * All Near Misses
         */
        List<NearMiss> nearMissList = wireMockServer.findNearMissesForAllUnmatchedRequests();
        keyList = Arrays.asList("Near Miss");
        rowList = nearMissList
                .stream()
                .map(m -> {
                    Map<String, Object> map = new HashMap<>();
                    map.put("Near Miss", m.toString().trim());
                    return map;
                })
                .toList();
        ZwsPrintTableHelper.printTable0("WireMock Near Misses", Pair.of(keyList, rowList));

    }

    private void register(StubMapping mapping) {
//            wireMockClient.register(mapping);
//        ZwsMockUtil.setMockedTimeout(mapping);
        wireMockServer.addStubMapping(mapping);
    }

    @com.fasterxml.jackson.annotation.JsonPropertyOrder({"loggedDate", "absoluteUrl", "method", "headers", "cookies", "queryParams", "body"})
    public static class LoggedRequestWrapper {
        @com.fasterxml.jackson.annotation.JsonIgnore private LoggedRequest request;
        public LoggedRequestWrapper(LoggedRequest request) { this.request = request; }
        @com.fasterxml.jackson.annotation.JsonProperty("absoluteUrl") public String getAbsoluteUrl() { return request.getAbsoluteUrl(); }
        @com.fasterxml.jackson.annotation.JsonProperty("method") public RequestMethod getMethod() { return request.getMethod(); }
        @com.fasterxml.jackson.annotation.JsonProperty("cookies") public Map<String, Cookie> getCookies() { return request.getCookies(); }
        @com.fasterxml.jackson.annotation.JsonProperty("loggedDate") public Date getLoggedDate() { return request.getLoggedDate(); }
        @com.fasterxml.jackson.annotation.JsonProperty("queryParams") public Map<String, QueryParameter> getQueryParams() { return request.getQueryParams(); }
        @com.fasterxml.jackson.annotation.JsonProperty("headers") public Map<String, Object> getHeaders() {
            Map<String, Object> headers = new HashMap<>();
            for (HttpHeader header: firstNonNull(request.getHeaders().all(), Collections.<HttpHeader>emptyList())) {
                if ("X-MDC".equalsIgnoreCase(header.key())) {
                    List<Object> values = header.values()
                            .stream()
                            .map(raw -> {
                                try {
                                    return Json.read(raw, Object.class);
                                } catch (Exception e) {
                                    // swallow it
                                }
                                return raw;
                            })
                            .toList();
                    headers.put(header.key(), values);
                } else {
                    headers.put(header.key(), header.values());
                }
            }
            return headers;
        }
        @com.fasterxml.jackson.annotation.JsonProperty("body") public Object getBody() {
            String raw = request.getBodyAsString();
            try {
                return Json.read(raw, Object.class);
            } catch (Exception e) {
                // swallow it
            }
            return raw;
        }
    }

    @com.fasterxml.jackson.annotation.JsonPropertyOrder({"status", "headers", "body", "fault"})
    public static class LoggedResponseWrapper {
        @com.fasterxml.jackson.annotation.JsonIgnore private LoggedResponse response;
        public LoggedResponseWrapper(LoggedResponse response) { this.response = response; }
        @com.fasterxml.jackson.annotation.JsonProperty("status") public int getStatus() { return response.getStatus(); }
        @com.fasterxml.jackson.annotation.JsonProperty("headers") public HttpHeaders getHeaders() { return response.getHeaders(); }
        @com.fasterxml.jackson.annotation.JsonProperty("fault") public Fault getFault() { return response.getFault(); }
        @com.fasterxml.jackson.annotation.JsonProperty("body") public Object getBody() {
            String raw = response.getBodyAsString();
            try {
                return Json.read(raw, Object.class);
            } catch (Exception e) {
                // swallow it
            }
            return raw;
        }
    }

}
